/*
 * Copyright (c) 2025 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the 'License');
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an 'AS IS' BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import {
  ArkAssignStmt,
  ArkInstanceFieldRef,
  ArkInstanceInvokeExpr,
  ArkNormalBinopExpr,
  Local,
  NormalBinaryOperator,
  Stmt,
  Type,
  Value,
} from 'arkanalyzer';

import Logger, { LOG_MODULE_TYPE } from 'arkanalyzer/lib/utils/logger';
import { MatcherCallback, MatcherTypes, StmtMatcher } from '../../matcher/Matchers';
import { Defects, IssueReport } from '../../model/Defects';
import { RuleFix } from '../../model/Fix';
import { Rule } from '../../model/Rule';
import { RuleListUtil } from '../../utils/common/DefectsList';
import { BaseChecker, BaseMetaData } from '../BaseChecker';
const logger = Logger.getLogger(LOG_MODULE_TYPE.HOMECHECK, 'PreferOptionalChainCheck');

// Options 接口保持不变
export type Options = [
  {
    /** Allow autofixers that will change the return type of the expression. This option is considered unsafe as it may break the build. */
    allowPotentiallyUnsafeFixesThatModifyTheReturnTypeIKnowWhatImDoing?: boolean;
    /** Check operands that are typed as `any` when inspecting "loose boolean" operands. */
    checkAny?: boolean;
    /** Check operands that are typed as `bigint` when inspecting "loose boolean" operands. */
    checkBigInt?: boolean;
    /** Check operands that are typed as `boolean` when inspecting "loose boolean" operands. */
    checkBoolean?: boolean;
    /** Check operands that are typed as `number` when inspecting "loose boolean" operands. */
    checkNumber?: boolean;
    /** Check operands that are typed as `string` when inspecting "loose boolean" operands. */
    checkString?: boolean;
    /** Check operands that are typed as `unknown` when inspecting "loose boolean" operands. */
    checkUnknown?: boolean;
    /** Skip operands that are not typed with `null` and/or `undefined` when inspecting "loose boolean" operands. */
    requireNullish?: boolean;
  }
];

export class PreferOptionalChainCheck implements BaseChecker {
  private defaultOptions: Options = [
    {
      checkAny: true,
      checkUnknown: true,
      checkString: true,
      checkNumber: true,
      checkBoolean: true,
      checkBigInt: true,
      requireNullish: false,
      allowPotentiallyUnsafeFixesThatModifyTheReturnTypeIKnowWhatImDoing: false,
    },
  ];
  public rule: Rule;
  public defects: Defects[] = [];
  public issues: IssueReport[] = [];
  public metaData: BaseMetaData = {
    severity: 2,
    ruleDocPath: 'docs/prefer-optional-chain.md',
    description: `Prefer using an optional chain expression instead, as it's more concise and easier to read.`,
  };
  private options: Options[0];
  private processedLogicalAndMap = new Map<string, Local>();
  private currentFilePath: string = '';
  private processedRanges = new Map<
    string,
    { startLine: number; startCol: number; endLine: number; endCol: number }[]
  >();

  private stmtMatcher: StmtMatcher = {
    matcherType: MatcherTypes.STMT,
    match(stmt: Stmt): boolean {
      return stmt instanceof ArkAssignStmt && stmt.getRightOp() instanceof ArkNormalBinopExpr;
    },
  };

  public registerMatchers(): MatcherCallback[] {
    const matchStmtCb: MatcherCallback = {
      matcher: this.stmtMatcher,
      callback: this.check,
    };
    return [matchStmtCb];
  }

  public check = (stmt: ArkAssignStmt): void => {
    this.options = this.rule?.option?.[0] ?? this.defaultOptions[0];
    const filePath = stmt.getCfg()?.getDeclaringMethod().getDeclaringArkFile().getFilePath() ?? '';

    if (this.currentFilePath !== filePath) {
      this.currentFilePath = filePath;
      this.processedRanges.clear();
    }
    const posInfo = stmt.getOriginPositionInfo();
    const line = posInfo.getLineNo();
    const { startLine, startCol, endLine, endCol } = this.getPosOfStmt(stmt);

    if (this.isProcessed(line, startLine, startCol, endLine, endCol)) {
      return;
    }

    const rightOp = stmt.getRightOp() as ArkNormalBinopExpr;

    // 检查逻辑与链： a && a.b
    if (rightOp.getOperator() === NormalBinaryOperator.LogicalAnd) {
      this.checkLogicalAndChain(rightOp, stmt);
    }
  };

  /**
   * 递归检查逻辑与链
   * @param expr 当前的二元表达式
   * @param originalStmt 原始语句，用于上报问题
   */
  private checkLogicalAndChain(expr: ArkNormalBinopExpr, originalStmt: ArkAssignStmt): void {
    const op1 = expr.getOp1();
    const op2 = expr.getOp2();

    // 识别核心模式: op1 && op1.property
    if (!(op1 instanceof Local) || !(op2 instanceof Local)) {
      return;
    }
    const { isPropertyAccess, firstOp } = this.getIfPropertyAccessAndFirstOp(op1, op2);
    if (isPropertyAccess) {
      // 检查类型是否符合 options
      if (this.shouldCheckType(firstOp.getType()) && this.shouldCheckType(op2.getType())) {
        const message = `Prefer using an optional chain expression instead of a logical AND.`;
        // 生成修复对象
        const fix = this.createFix(originalStmt, expr, firstOp);
        this.addIssueReport(originalStmt, message, fix);
      }
    }
  }

  private getIfPropertyAccessAndFirstOp(op1: Local, op2: Local): { isPropertyAccess: boolean; firstOp: Local } {
    const op2DeclaringStmt = op2.getDeclaringStmt();
    if (!(op2DeclaringStmt instanceof ArkAssignStmt)) {
      return { isPropertyAccess: false, firstOp: op1 };
    }
    const op2RightOp = op2DeclaringStmt.getRightOp();
    if (!(op2RightOp instanceof ArkInstanceFieldRef) && !(op2RightOp instanceof ArkInstanceInvokeExpr)) {
      return { isPropertyAccess: false, firstOp: op1 };
    }
    let processedOp = this.getFrontBaseNode(op1);
    if (processedOp === undefined) {
      return { isPropertyAccess: false, firstOp: op1 };
    }
    return { isPropertyAccess: this.isSameBaseNode(processedOp, op2RightOp.getBase()), firstOp: processedOp };
  }

  private getFrontBaseNode(op: Local): Local | undefined {
    if (op.getConstFlag() === true) {
      return op;
    }
    let processedOp = this.processedLogicalAndMap.get(op.getName());
    if (processedOp === undefined) {
      processedOp = this.getFrontOp(op);
    }
    return processedOp;
  }

  private getFrontOp(op: Local): Local | undefined {
    let processedOp: Local | undefined;
    const opDeclaringStmt = op.getDeclaringStmt();
    if (opDeclaringStmt instanceof ArkAssignStmt) {
      const opRightOp = opDeclaringStmt.getRightOp();
      if (
        opRightOp instanceof ArkNormalBinopExpr &&
        opRightOp.getOperator() === NormalBinaryOperator.LogicalAnd &&
        opRightOp.getOp2() instanceof Local
      ) {
        processedOp = opRightOp.getOp2() as Local;
      } else if (
        opRightOp instanceof Local ||
        opRightOp instanceof ArkInstanceFieldRef ||
        opRightOp instanceof ArkInstanceInvokeExpr
      ) {
        return op;
      }
    }
    return processedOp;
  }

  private escapeRegExp(string: string): string {
    return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
  }

  /**
   * 创建修复对象
   */
  private createFix(stmt: ArkAssignStmt, expr: ArkNormalBinopExpr, firstOp: Local): RuleFix | undefined {
    const { startLine, startCol, endLine, endCol } = this.getPosOfStmt(stmt);
    const posInfo = stmt.getOriginPositionInfo();
    const line = posInfo.getLineNo();
    this.addProcessedRange(line, startLine, startCol, endLine, endCol);
    const fixInfo = this.buildOptionalChainFix(stmt, expr, firstOp);
    if (!fixInfo) {
      return undefined;
    }
    const originalText = stmt.getOriginalText() ?? '';
    const fixedText = fixInfo.fixedText.split('.').join('?.');

    // 从 originalStmts 数组构建正则表达式，以匹配原始的 "&&" 链
    // 例如, ['foo', 'foo.a'] 会生成 /foo\s*&&\s*foo\.a/
    const pattern = fixInfo.originalStmts.map((s) => this.escapeRegExp(s)).join('\\s*&&\\s*');
    const regex = new RegExp(pattern);

    const match = regex.exec(originalText);
    if (!match) {
      return undefined;
    }
    const start = posInfo.getColNo() + match.index;
    const end = start + match[0].length;

    return { range: [start, end], text: fixedText };
  }

  /**
   * 构建可选链修复文本的递归函数
   */
  private buildOptionalChainFix(
    stmt: ArkAssignStmt,
    expr: ArkNormalBinopExpr,
    firstOp: Local
  ): { fixedText: string; originalStmts: string[] } | null {
    if (!(expr.getOp2() instanceof Local) || !(stmt.getLeftOp() instanceof Local)) {
      return null;
    }
    const op2 = expr.getOp2() as Local;
    const left = stmt.getLeftOp() as Local;
    if (firstOp === undefined) {
      return null;
    }

    const originalStmts: string[] = [];
    originalStmts.push(this.getFixedStringOfLocal(firstOp));

    this.processedLogicalAndMap.set(left.getName(), op2);

    // 处理右侧
    this.getRightStmtRecursive(originalStmts, expr, op2, left);
    if (originalStmts.length > 1) {
      return {
        fixedText: originalStmts[originalStmts.length - 1],
        originalStmts,
      };
    }
    return null;
  }

  private getFixedStringOfLocal(op: Local): string {
    const declaringStmt = op.getDeclaringStmt();
    if (!(declaringStmt instanceof ArkAssignStmt)) {
      return '';
    }
    const fieldRef = declaringStmt.getRightOp();
    if (op.getConstFlag() === true) {
      return op.getName();
    } else if (fieldRef instanceof ArkInstanceFieldRef) {
      return `${this.getFixedStringOfLocal(fieldRef.getBase())}.${fieldRef.getFieldSignature().getFieldName()}`;
    } else if (fieldRef instanceof ArkInstanceInvokeExpr) {
      return `${this.getFixedStringOfLocal(fieldRef.getBase())}.${fieldRef
        .getMethodSignature()
        .getMethodSubSignature()
        .getMethodName()}(${fieldRef
        .getArgs()
        .map((arg) => arg.toString())
        .join(', ')})`;
    }
    return '';
  }

  // 递归处理右侧语句（合并逻辑，简化参数传递）
  private getRightStmtRecursive(originalStmts: string[], expr: ArkNormalBinopExpr, _op: Local, _left: Local): void {
    const opDeclaringStmt = _op.getDeclaringStmt();
    if (!(opDeclaringStmt instanceof ArkAssignStmt)) {
      return;
    }
    const op2RightOp = opDeclaringStmt.getRightOp();
    // 使用提取的字符串拼接函数
    const accessStr = this.buildAccessString(originalStmts[originalStmts.length - 1], op2RightOp);
    if (accessStr !== originalStmts[originalStmts.length - 1]) {
      originalStmts.push(accessStr);
    }

    // 异常处理：检查 getUsedStmts() 是否为空
    const usedStmts = _left.getUsedStmts();
    if (usedStmts.length === 0) {
      return;
    }
    const leftUsedStmt = usedStmts[0];
    if (leftUsedStmt instanceof ArkAssignStmt) {
      const info = this.checkNextLogicalAndChain(leftUsedStmt);
      if (!info) {
        return;
      }
      this.updateProcessedRangeWithStmt(leftUsedStmt);
      this.getRightStmtRecursive(originalStmts, expr, info.op2, info.left);
    }
  }

  // 字符串拼接工具函数
  private buildAccessString(base: string, rightOp: Value): string {
    if (rightOp instanceof ArkInstanceFieldRef) {
      return `${base}.${rightOp.getFieldSignature().getFieldName()}`;
    } else if (rightOp instanceof ArkInstanceInvokeExpr) {
      const methodName = rightOp.getMethodSignature().getMethodSubSignature().getMethodName();
      const args = rightOp
        .getArgs()
        .map((arg) => arg.toString())
        .join(', ');
      return `${base}.${methodName}(${args})`;
    }
    return base;
  }

  private checkNextLogicalAndChain(leftUsedStmt: ArkAssignStmt): { op2: Local; left: Local } | null {
    const right = leftUsedStmt.getRightOp();
    const left = leftUsedStmt.getLeftOp();
    if (
      left instanceof Local &&
      right instanceof ArkNormalBinopExpr &&
      right.getOperator() === NormalBinaryOperator.LogicalAnd
    ) {
      const op1 = right.getOp1();
      const op2 = right.getOp2();

      if (!(op1 instanceof Local) || !(op2 instanceof Local)) {
        return null;
      }
      if (this.getIfPropertyAccessAndFirstOp(op1, op2).isPropertyAccess) {
        if (this.shouldCheckType(op1.getType())) {
          this.processedLogicalAndMap.set(left.getName(), op2);
          return { op2, left };
        }
      }
    }
    return null;
  }

  private isSameBaseNode(value1: Local, value2: Local): boolean {
    if (!value1 || !value2) {
      return false;
    }
    if (value1.getConstFlag() === true || value2.getConstFlag() === true) {
      return value1.getName() === value2.getName();
    }
    if (value1.getType().getTypeString() !== value2.getType().getTypeString()) {
      return false;
    }
    const rightOp1 = this.getRightOp(value1);
    const rightOp2 = this.getRightOp(value2);
    if (!rightOp1 || !rightOp2) {
      return false;
    }
    if (rightOp1 instanceof Local && rightOp2 instanceof Local) {
      return rightOp1.getType().getTypeString() === rightOp2.getType().getTypeString();
    } else if (rightOp1 instanceof ArkInstanceFieldRef && rightOp2 instanceof ArkInstanceFieldRef) {
      return this.isSameArkInstanceFieldRef(rightOp1, rightOp2);
    } else if (rightOp1 instanceof ArkInstanceInvokeExpr && rightOp2 instanceof ArkInstanceInvokeExpr) {
      return this.isSameArkInstanceInvokeExpr(rightOp1, rightOp2);
    }
    return false;
  }

  private getRightOp(value: Local): Value | null {
    const stmt = value.getDeclaringStmt();
    if (!stmt) {
      return null;
    }
    if (!(stmt instanceof ArkAssignStmt)) {
      return null;
    }
    return stmt.getRightOp();
  }

  private isSameArkInstanceFieldRef(value1: ArkInstanceFieldRef, value2: ArkInstanceFieldRef): boolean {
    if (!value1 || !value2) {
      return false;
    }
    const fieldSignature1 = value1.getFieldSignature().toString();
    const fieldSignature2 = value2.getFieldSignature().toString();
    if (fieldSignature1 === fieldSignature2) {
      return this.isSameBaseNode(value1.getBase(), value2.getBase());
    }
    return false;
  }

  private isSameArkInstanceInvokeExpr(value1: ArkInstanceInvokeExpr, value2: ArkInstanceInvokeExpr): boolean {
    if (!value1 || !value2) {
      return false;
    }
    const params1 = value1.getArgs().map((arg) => arg.toString());
    const params2 = value2.getArgs().map((arg) => arg.toString());
    if (
      value1.getMethodSignature().isMatch(value2.getMethodSignature()) &&
      params1.length === params2.length &&
      params1.every((val, idx) => val === params2[idx])
    ) {
      return this.isSameBaseNode(value1.getBase(), value2.getBase());
    }
    return false;
  }

  /**
   * 根据 options 判断是否应该检查此类型
   */
  private shouldCheckType(type: Type): boolean {
    if (!type) {
      return this.options.checkUnknown ?? false;
    }

    const typeName = type.getTypeString();
    if (typeName === 'any') {
      return this.options.checkAny ?? false;
    }
    if (typeName === 'unknown') {
      return this.options.checkUnknown ?? false;
    }

    // 如果 requireNullish 为 true，则类型必须包含 null 或 undefined
    const isNullish = typeName.includes('null') || typeName.includes('undefined');
    if (this.options.requireNullish && !isNullish) {
      return false;
    }

    const primitiveChecks: Record<string, boolean | undefined> = {
      string: this.options.checkString,
      number: this.options.checkNumber,
      boolean: this.options.checkBoolean,
      bigint: this.options.checkBigInt,
    };
    for (const [prim, check] of Object.entries(primitiveChecks)) {
      if (typeName.includes(prim)) {
        return check ?? false;
      }
    }
    return true;
  }

  /**
   * 检查指定范围是否已被处理（包含在已有范围中）
   */
  private isProcessed(line: number, startLine: number, startCol: number, endLine: number, endCol: number): boolean {
    // Key 为 "行号"，锚定起始点
    const key = `${line}`;
    const ranges = this.processedRanges.get(key);
    if (!ranges) {
      return false;
    }
    // 检查当前范围是否被该起始点下的某个已有范围包含
    return ranges.some(
      (range) =>
        startLine >= range.startLine && startCol >= range.startCol && endLine <= range.endLine && endCol <= range.endCol
    );
  }

  private getPosOfStmt(stmt: ArkAssignStmt): { startLine: number; startCol: number; endLine: number; endCol: number } {
    const posInfo = stmt.getOriginPositionInfo();

    const line = posInfo.getLineNo();
    let startLine = line;
    let startCol = posInfo.getColNo();
    let endLine = line;
    let endCol = startCol + (stmt.getOriginalText()?.length ?? 0);
    const startOpPos = stmt.getOperandOriginalPosition(stmt.getLeftOp());
    const endOpPose = stmt.getOperandOriginalPosition(stmt.getRightOp());
    if (startOpPos) {
      startLine = startOpPos.getFirstLine();
      startCol = startOpPos.getFirstCol();
    }
    if (endOpPose) {
      endLine = endOpPose.getLastLine();
      endCol = endOpPose.getLastCol();
    }
    return { startLine, startCol, endLine, endCol };
  }

  private updateProcessedRangeWithStmt(stmt: ArkAssignStmt): void {
    const { startLine, startCol, endLine, endCol } = this.getPosOfStmt(stmt);
    const key = `${startLine}`;
    const ranges = this.processedRanges.get(key);
    if (!ranges) {
      return;
    }
    for (const range of ranges) {
      if (startLine === range.startLine && startCol === range.startCol) {
        range.endLine = endLine;
        range.endCol = endCol;
      }
    }
  }

  /**
   * 记录已处理的范围
   */
  private addProcessedRange(line: number, startLine: number, startCol: number, endLine: number, endCol: number): void {
    const key = `${line}`;
    let ranges = this.processedRanges.get(key);
    if (!ranges) {
      ranges = [];
      this.processedRanges.set(key, ranges);
    }
    ranges.push({ startLine, startCol, endLine, endCol });
  }

  /**
   * addIssueReport 接收RuleFix
   */
  private addIssueReport(stmt: ArkAssignStmt, message: string, fix?: RuleFix): void {
    this.processedLogicalAndMap.clear();
    const key = stmt.getOriginPositionInfo().getLineNo().toString();
    const ranges = this.processedRanges.get(key);
    let { startLine, startCol, endLine, endCol } = this.getPosOfStmt(stmt);
    for (const range of ranges ?? []) {
      if (startLine === range.startLine && startCol === range.startCol) {
        endLine = range.endLine;
        endCol = range.endCol;
      }
    }

    const severity = this.rule.alert ?? this.metaData.severity;

    const filePath = stmt.getCfg()?.getDeclaringMethod().getDeclaringArkFile().getFilePath() ?? '';

    const fixable = fix !== undefined;
    const defect = new Defects(
      startLine,
      startCol,
      endCol,
      message,
      severity,
      this.rule.ruleId,
      filePath,
      this.metaData.ruleDocPath,
      true,
      false,
      fixable
    );
    this.issues.push(new IssueReport(defect, fix));
    RuleListUtil.push(defect);
  }
}
